Думаю, все мы себе представляем, как работает термопринтер. Но неподвижная пластина и ряд нагревательных элементов не всегда были типичной конструкцией такого устройства.
Итак, в сегодняшней статье разберёмся, как устроен и работает термопринтер старого образца с подвижной головкой. Узнаем, как его подключить к микроконтроллеру и запустить. Традиционно будет много интересного.
❯ Суть такова
Давным-давно я уже писал пост о подключении термопечатающей головки к микроконтроллеру. И там я обмолвился, что существовали и более старые экземпляры, где головка не была неподвижной и которые сильно отличаются по управлению. Конечно, с моей стороны было бы неправильно упустить из виду такой экземпляр, тем более, что у меня он есть.
❯ Обзор оборудования
Так уж получилось, что мне в своё время достались остатки от мониторов пациента компании Criticare (модель мне неизвестна, но, судя по всему, это 506N3). Измерительное оборудование было утрачено, но осталась горсть плат, а также несколько термопринтеров.
Сама плата. Запустить её мне не удалось, при включении она просто выдаёт какую-то ошибку, попутно сообщая, что датчик пульсоксиметра не подключён. Тем не менее, распаивать её я не буду, когда-нибудь мы к ней ещё вернёмся.
Вообще, медицинское оборудование само по себе — отдельная тема, заслуживающая далеко не одной статьи. Многие из этих девайсов очень крутые, а некоторые технологические решения, что там можно встретить, сильно удивляют.
А вот и термопринтер. Это STP211J-192 от Seiko/Epson. Как ясно из названия, разрешение по горизонтали у него 192 точки. Отчётливо видны два шаговых двигателя, печатающая головка, направляющая, червячный вал.
С обратной стороны ничего интересного.
Слева привод головки. Также тут находится концевой выключатель крайнего её положения.
Справа привод протяжки бумаги.
Из других устройств, где применялись такие термопринтеры, можно вспомнить VeriFone PrintPak. И если в модели 350 стоит самый обычный, то в более старом 300 — именно тот, что у нас. Мною весьма активно ищется такой аппарат, но пока что найти его не вышло.
❯ Что нужно, чтобы управлять таким принтером?
В отличие от ранее рассмотренных экземпляров, этот простой и дубовый как никогда.
Из оборудования у нас имеются два мотора, головка, а также датчик её положения. Всё, больше ничего нет. Сама по себе головка представляет сборку из восьми резисторов, которые и служат нагревателями, никакой управляющей логики в ней нет.
Таким образом, помимо драйвера двигателей, понадобятся также силовые ключи для управления головкой.
❯ Моторы
Поскольку шаговые двигатели тут униполярные, для управления ими было решено использовать ULN2804A. Восьми выходов как раз хватит для двух шаговиков, использующихся в принтере.
В даташите на принтер отыскались и последовательности включения двигателя. Так что проблем возникнуть не должно.
Помня об этом, подключаем моторы к ULNке. Выводы 1-8 соединяются с портами контроллера.
❯ ТПГ
В отличие от более совершенных моделей, где термопечатающая головка имела свой собственный драйвер и управлялась по последовательному интерфейсу, здесь применена обычная сборка из восьми нагревательных резисторов. Сама головка съёмная, в даташите даже описана процедура её замены.
Сопротивление этих резисторов отличается в зависимости от модели принтера и составляет от четырнадцати до восемнадцати ом.
Итак, схема для управления головкой получается примерно такая.
❯ Контроллер
Для управления решил взять всем известную Arduino — просто из-за пятивольтовых уровней и встроенного USB-UART. У меня нет ответной части к такому шлейфу, поэтому я припаял МГТФ прямо к контактам. Они там очень крупные, можно спокойно подпаяться, не боясь поплавить шлейф.
Собираем всё вместе. Термопринтер просто идеально подошёл по размерам на макетку. На ней же разместились преобразователь питания, две ULNки и плата Arduino. Термоголовка питается от пяти вольт, но брать их от USB нельзя, во время печати ток может составлять больше двух ампер. Всё, можно начинать эксперименты.
❯ Управление моторами
И для начала, конечно, разберёмся с приводами. Тут всё достаточно просто — шаг мотора головки сдвигает её на расстояние одного пикселя, шаг мотора протяжки бумаги прокручивает её на расстояние четверти пикселя. Функции для всего этого получились вот такие:
uint8_t currentPhase = 0; uint8_t paperCurrentPhase = 0; void paperStep() { switch (paperCurrentPhase) { case 2: digitalWrite(A0, LOW); digitalWrite(A1, LOW); digitalWrite(A2, HIGH); digitalWrite(A3, HIGH); break; case 3: digitalWrite(A0, LOW); digitalWrite(A1, HIGH); digitalWrite(A2, HIGH); digitalWrite(A3, LOW); break; case 0: digitalWrite(A0, HIGH); digitalWrite(A1, HIGH); digitalWrite(A2, LOW); digitalWrite(A3, LOW); break; case 1: digitalWrite(A0, HIGH); digitalWrite(A1, LOW); digitalWrite(A2, LOW); digitalWrite(A3, HIGH); break; } if (paperCurrentPhase == 3) paperCurrentPhase = 0; else paperCurrentPhase++; } void headStep(int8_t dir) { if (dir == -1) { switch (currentPhase) { case 1: digitalWrite(A4, LOW); digitalWrite(A5, LOW); digitalWrite(11, HIGH); digitalWrite(12, HIGH); break; case 0: digitalWrite(A4, LOW); digitalWrite(A5, HIGH); digitalWrite(11, HIGH); digitalWrite(12, LOW); break; case 3: digitalWrite(A4, HIGH); digitalWrite(A5, HIGH); digitalWrite(11, LOW); digitalWrite(12, LOW); break; case 2: digitalWrite(A4, HIGH); digitalWrite(A5, LOW); digitalWrite(11, LOW); digitalWrite(12, HIGH); break; } } else if (dir == 1) { switch (currentPhase) { case 0: digitalWrite(A4, LOW); digitalWrite(A5, LOW); digitalWrite(11, HIGH); digitalWrite(12, HIGH); break; case 1: digitalWrite(A4, LOW); digitalWrite(A5, HIGH); digitalWrite(11, HIGH); digitalWrite(12, LOW); break; case 2: digitalWrite(A4, HIGH); digitalWrite(A5, HIGH); digitalWrite(11, LOW); digitalWrite(12, LOW); break; case 3: digitalWrite(A4, HIGH); digitalWrite(A5, LOW); digitalWrite(11, LOW); digitalWrite(12, HIGH); break; } } if (currentPhase == 3) currentPhase = 0; else currentPhase++; }
В отличие от управления головкой, время выполнения тут не слишком критично, поэтому используются «медленные» digitalWrite. Для привода ТПГ также добавлена возможность задания направления.
❯ Инициализация
Отдельно стоит упомянуть про действия после запуска. Сразу после подачи питания МК не знает, где сейчас находится головка. Поэтому необходимо выставить её в нулевое положение — гнать влево, пока она не упрётся в концевой выключатель. Дальше необходимо сделать ещё несколько добавочных шагов, так как датчик срабатывает несколько раньше, чем головка упирается в крайнее положение. Если же ноль уже стоит, выводим головку из него и проверяем, не разомкнулся ли концевик. Если даже после существенного числа шагов он всё равно замкнут, значит, на моторы не подаётся питание или просто нет контакта.
Делается это всё примерно так:
void headInit() { if (!digitalRead(10)) headReturn(); else { for (int i = 0; i < 50; i++) { headStep(1); delay(10); } if (digitalRead(10)) { Serial.println("Head drive error"); while (1);; } else headReturn(); } } void headReturn() { while (!digitalRead(10)) { headStep(-1); delay(10); } for (int i = 0; i < 6; i++) { headStep(-1); delay(10); } }
Вообще, в даташите было сказано о двух шагах после касания концевика. Но в моём случае механизм имел достаточно сильный люфт, так что для уверенного возврата каретки число шагов увеличил до шести. Только тогда она стала нормально вставать в крайнее положение.
❯ Управление головкой
Теперь очередь нагревателей. Чтобы задать их состояние, используется следующая функция:
void headControl(uint8_t toHead) { PORTD &= B00000011; PORTB &= B11111100; PORTD |= ((toHead << 2) & B11111100); PORTB |= ((toHead >> 6) & B00000011); }
Для удобства загрузки восьми бит сразу применена работа с портами через регистры.
❯ Печать символов
Как известно, головка печатает строку символов за один проход, как в матричном принтере. То есть за раз прожигается вертикальная линия из восьми точек. А это значит, что тот самый шрифт из предыдущего поста про термопринтер подойдёт как нельзя лучше, не придётся разбираться с преобразованием строки символов в восемь горизонтальных линий. Поэтому для печати символа необходимо всего лишь разбить его на пять столбцов, а потом последовательно прожечь их, каждый раз сдвигая головку на один шаг.
Делается это примерно так:
void printChar(char input) { uint8_t vertical8dots = 0x00; for (int i = 0; i < 5; i++) { vertical8dots = pgm_read_byte(&FontTable[input][i]); headControl(vertical8dots); delay(3); headControl(0x00); headStep(1); delay(10); } headDriveOff(); }
Задержка перед отключением головки определяет яркость печати. Не стоит пытаться изменить её поднятием напряжения, иначе головка может сдохнуть.
❯ Печать строки
Ну, где символы, там и строка. Делается это всё достаточно просто:
void printString(String toPrinter) { int target = 0; for (int i = 0; i < 20; i++) { headStep(1); delay(10); } if (toPrinter.length() > 18) target = 18; else target = toPrinter.length(); for (int i = 0; i < target; i++) { printChar(toPrinter[i]); for (int n = 0; n < 3; n++) { headStep(1); delay(10); } } headReturn(); headDriveOff(); }
Ничего сложного: прожигаем очередной символ, затем сдвигаем головку на некоторое число пикселей (в моём случае три) и так до конца строки. Затем возвращаем головку на место, и можно проматывать бумагу.
В итоге вся программа получилась такая:
#include "FontTable.h" uint8_t currentPhase = 0; uint8_t paperCurrentPhase = 0; void setup() { for (int i = 2; i <= 9; i++) pinMode(i, OUTPUT); pinMode(10, INPUT_PULLUP); pinMode(11, OUTPUT); pinMode(12, OUTPUT); for (int i = 14; i <= 19; i++) pinMode(i, OUTPUT); Serial.begin(115200); headInit(); headDriveOff(); } void paperStep() { switch (paperCurrentPhase) { case 2: digitalWrite(A0, LOW); digitalWrite(A1, LOW); digitalWrite(A2, HIGH); digitalWrite(A3, HIGH); break; case 3: digitalWrite(A0, LOW); digitalWrite(A1, HIGH); digitalWrite(A2, HIGH); digitalWrite(A3, LOW); break; case 0: digitalWrite(A0, HIGH); digitalWrite(A1, HIGH); digitalWrite(A2, LOW); digitalWrite(A3, LOW); break; case 1: digitalWrite(A0, HIGH); digitalWrite(A1, LOW); digitalWrite(A2, LOW); digitalWrite(A3, HIGH); break; } if (paperCurrentPhase == 3) paperCurrentPhase = 0; else paperCurrentPhase++; } void headStep(int8_t dir) { if (dir == -1) { switch (currentPhase) { case 1: digitalWrite(A4, LOW); digitalWrite(A5, LOW); digitalWrite(11, HIGH); digitalWrite(12, HIGH); break; case 0: digitalWrite(A4, LOW); digitalWrite(A5, HIGH); digitalWrite(11, HIGH); digitalWrite(12, LOW); break; case 3: digitalWrite(A4, HIGH); digitalWrite(A5, HIGH); digitalWrite(11, LOW); digitalWrite(12, LOW); break; case 2: digitalWrite(A4, HIGH); digitalWrite(A5, LOW); digitalWrite(11, LOW); digitalWrite(12, HIGH); break; } } else if (dir == 1) { switch (currentPhase) { case 0: digitalWrite(A4, LOW); digitalWrite(A5, LOW); digitalWrite(11, HIGH); digitalWrite(12, HIGH); break; case 1: digitalWrite(A4, LOW); digitalWrite(A5, HIGH); digitalWrite(11, HIGH); digitalWrite(12, LOW); break; case 2: digitalWrite(A4, HIGH); digitalWrite(A5, HIGH); digitalWrite(11, LOW); digitalWrite(12, LOW); break; case 3: digitalWrite(A4, HIGH); digitalWrite(A5, LOW); digitalWrite(11, LOW); digitalWrite(12, HIGH); break; } } if (currentPhase == 3) currentPhase = 0; else currentPhase++; } void lineFeed() { for (int i = 0; i < 48; i++) { paperStep(); delay(10); } paperDriveOff(); } void headReturn() { while (!digitalRead(10)) { headStep(-1); delay(10); } for (int i = 0; i < 6; i++) { headStep(-1); delay(10); } } void headInit() { if (!digitalRead(10)) headReturn(); else { for (int i = 0; i < 50; i++) { headStep(1); delay(10); } if (digitalRead(10)) { Serial.println("Head drive error"); while (1);; } else headReturn(); } } void headDriveOff() { digitalWrite(A4, LOW); digitalWrite(A5, LOW); digitalWrite(11, LOW); digitalWrite(12, LOW); } void paperDriveOff() { digitalWrite(A0, LOW); digitalWrite(A1, LOW); digitalWrite(A2, LOW); digitalWrite(A3, LOW); } void headControl(uint8_t toHead) { PORTD &= B00000011; PORTB &= B11111100; PORTD |= ((toHead << 2) & B11111100); PORTB |= ((toHead >> 6) & B00000011); } void printChar(char input) { uint8_t vertical8dots = 0x00; for (int i = 0; i < 5; i++) { vertical8dots = pgm_read_byte(&FontTable[input][i]); headControl(vertical8dots); delay(3); headControl(0x00); headStep(1); delay(10); } headDriveOff(); } void printString(String toPrinter) { int target = 0; for (int i = 0; i < 20; i++) { headStep(1); delay(10); } if (toPrinter.length() > 18) target = 18; else target = toPrinter.length(); for (int i = 0; i < target; i++) { printChar(toPrinter[i]); for (int n = 0; n < 3; n++) { headStep(1); delay(10); } } headReturn(); headDriveOff(); } void loop() { String inputString = Serial.readString(); if (inputString.length() > 0) { printString(inputString); lineFeed(); } }
Пробуем что-то напечатать… и оно даже работает! К слову говоря, шрифт очень сильно напоминает тот, что выдаёт матричный принтер. Справа на фото как раз такая распечатка — сходство весьма сильное.
И я даже записал видео с этим:
❯ Двунаправленная печать
А что, если реализовать печать как в матричном принтере — при каждом проходе каретки? Официально этот механизм такое не поддерживает, но ничего не мешает это попробовать.
Для того, чтобы такое реализовать, необходимо поменять алгоритм печати: будем прожигать строку не посимвольно, а всю разом. Для этого создадим массив, куда запишем все символы вместе с пробелами сразу, а потом будем его печатать. Получилось примерно следующее:
void printString(String toPrinter) { uint8_t index = 0; uint8_t toHead[192]; for (int i = 0; i < 192; i++) toHead[i] = 0x00; int target = 0; for (int i = 0; i < 20; i++) { headStep(1); delay(10); } if (toPrinter.length() > 18) target = 18; else target = toPrinter.length(); for (int i = 0; i < target; i++) { for (int n = 0; n < 5; n++) { toHead[index] = pgm_read_byte(&FontTable[toPrinter[i]][n]); index++; } index += 3; } for (int i = 0; i < 146; i++) { headControl(toHead[i]); delay(3); headControl(0x00); headStep(1); delay(10); } headMoveDirection = -1; headDriveOff(); }
Как оказалось, эффективная ширина печати намного меньше 192 пикселей, а без отступов по краям распечатка выглядит так себе. Тем не менее, размер массива в 192 байта я оставил, для совместимости с другими модификациями этого принтера (ну, или если кому-то захочется печатать без полей).
Запускаем и убеждаемся, что всё работает как надо. Как нетрудно догадаться, алгоритм печати в обратном направлении совершенно идентичен:
void printStringReversed(String toPrinter) { uint8_t index = 0; uint8_t toHead[192]; for (int i = 0; i < 192; i++) toHead[i] = 0x00; int target = 0; if (toPrinter.length() > 18) target = 18; else target = toPrinter.length(); for (int i = 0; i < target; i++) { for (int n = 0; n < 5; n++) { toHead[index] = pgm_read_byte(&FontTable[toPrinter[i]][n]); index++; } index += 3; } for (int i = 148; i >= 0; i--) { headControl(toHead[i]); delay(3); headControl(0x00); headStep(-1); delay(10); } headReturn(); headDriveOff(); headMoveDirection = 1; }
Чтобы сделать печать максимально простой, добавил отдельную функцию, где бы выбиралось нужное направление:
void processPrinting(String input) { if (headMoveDirection == 1) printString(input); else if (headMoveDirection == -1) printStringReversed(input); lineFeed(); }
Работает. Но тут я столкнулся уже с чисто конструктивными ограничениями: как бы я ни подкручивал общее число шагов, заставить строки быть точно на одном уровне не вышло. Люфт механизма всё же даёт о себе знать.
Получилось примерно так:
На видео заметен ещё один глюк: при печати в обратном направлении теряются первые два символа. Это косяк не алгоритма печати, а исключительно функции для вывода этой таблицы символов.
❯ Вот как-то так
Понятное дело, что в наши дни этот принтер — скорее игрушка, чем действительно рабочий девайс. И для реальных проектов давно уже существуют более простые в управлении принтеры. Тем не менее, запустить такое было реально интересно. А некоторое сходство с матричным принтером ещё больше добавляет крутизны этому девайсу.
Такие дела.
Возможно, захочется почитать и это:
ссылка на оригинал статьи https://habr.com/ru/articles/752870/